package logic;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 
 * @author Dastan Yessekeyev
 * 
 */
public class DeadlineParser {

	/**
	 * Tries 3 different acceptable formats.
	 * 
	 * @param deadline
	 * @return Returns the Date Value of the deadline(String).
	 * @throws Exception
	 *           if deadline does not match any of the formats
	 */
	public Date parseString(String deadline) throws Exception {
		String er = "";
		deadline = getRidOfRedundantSpacesAndArticles(deadline).toLowerCase();
		try {
			return getValue(deadline).getTime();
		} catch (Exception e) {
			er += e.getMessage();
		}
		try {
			String range = deadline;
			if (range.startsWith("within"))
				range = range.replaceFirst("within", "");

			if (range.startsWith("in"))
				range = range.replaceFirst("in", "");

			return getValue(range + " from now").getTime();
		} catch (Exception e) {
			er += e.getMessage();
		}
		try {
			return getValue("due " + deadline).getTime();
		} catch (Exception e) {
			er += e.getMessage();
		}

		throw new Exception("[DeadlineParser].parseString(): Unable to parse "
				+ deadline + "\n" + er);

	}

	private boolean timeAdjustmentIsNeeded;

	private Calendar getReference(String reference, int limit) throws Exception {
		if (reference.trim().equals("now"))
			return Calendar.getInstance();
		;

		Calendar ref = matchForExactDate(reference, limit);

		return ref;
	}

	private static final String[] specialDay = { "today", "yesterday",
			"day after tomorrow", "tomorrow" };
	private static final int[] specialDaysReference = { 0, -1, 2, 1 };

	private static final int YEAR_PRIME = 7, MONTH_PRIME = 5, DAY_PRIME = 3,
			TIME_PRIME = 2;
	private static int precision, todayIs;
	private static final String[] format = { " <(\\d{1,2})> (\\d{1,2}) ",
			" (\\d{1,2}) of <(\\d{1,2})> ", " (\\d{1,2}) <(\\d{1,2})> ",
			" ([1][0-1]):([0-5][0-9])[ap][m] ", " ([0-9]):([0-5][0-9])[ap][m] ",
			" ([0-9]):([0-5][0-9]) ", " ([1][0-9]):([0-5][0-9]) ",
			" ([2][0-3]):([0-5][0-9]) ", " ([1][0-1])[ap][m] ", " ([0-9])[ap][m] ",
			" this \\[([1-7])\\] ", " next \\[([1-7])\\] ", " (\\d{4}) ",
			" <(\\d{1,2})> " };
	private static final int[] formatType = { 1, 2, 2, 3, 3, 3, 3, 3, 4, 4, 5, 6,
			7, 8 };

	private Calendar result;

	/**
	 * 
	 * @param arg
	 * @return the Calendar value of the day arg(String)
	 * @throws Exception
	 *           In case of unacceptable format
	 */
	private Calendar matchForExactDate(String arg, int limit) throws Exception {
		precision = YEAR_PRIME * MONTH_PRIME * DAY_PRIME * TIME_PRIME;
		result = Calendar.getInstance();
		if (arg.contains(">") || arg.contains("<") || arg.contains("[")
				|| arg.contains("]"))
			throw new Exception(
					"[DeadlineParser].matchForExactDate(): Unacceptable Format");

		arg = remake(arg);

		assert (specialDay.length == specialDaysReference.length) : "adjust specialDays in DeadlineParser";

		for (int i = 0; i < specialDay.length; i++)
			if (arg.contains(specialDay[i])) {
				result.add(Calendar.DAY_OF_YEAR, specialDaysReference[i]);
				arg = arg.replaceFirst(specialDay[i], "");
				assignPrecision(DAY_PRIME * MONTH_PRIME * YEAR_PRIME);
				break;
			}
		arg = " " + arg + " ";

		assert (format.length == formatType.length) : "adjust format with formatType in DeadlineParser";

		for (int i = 0; i < format.length; i++)
			arg = check(format[i], arg, formatType[i]);

		if (precision % YEAR_PRIME == 0)
			assignPrecision(YEAR_PRIME);

		if ((precision % TIME_PRIME != 0) && (precision != 1))
			throw new Exception(
					"[DeadlineParser].matchForExactDate(): Specified Time but no Date");
		if (!arg.trim().isEmpty())
			throw new Exception(
					"[DeadlineParser].matchForExactDate(): Unidentifiabe elements -> "
							+ arg);

		return adjustTime(result, limit);
	}

	/**
	 * This function parses chunks of the String arg according to the
	 * formatType(groupNumber). Upon successful matching of the format(regex), it
	 * parses the matched piece of string and gradually updates Calendar result
	 * according to the formatType(groupNumber) and extracts the piece from arg
	 * afterwards
	 * 
	 * @param regex
	 * @param arg
	 * @param groupNumber
	 *          is a formatType
	 * @return modified arg
	 * @throws Exception
	 *           in case of Date or Time Overflow
	 */
	private static Pattern pattern;
	private static Matcher matcher;

	private String check(String regex, String arg, int groupNumber)
			throws Exception {
		pattern = Pattern.compile(regex);
		matcher = pattern.matcher(arg);
		if (matcher.find()) {
			int shift, year, month, day, hour, minute = 0;
			Calendar temp = Calendar.getInstance();
			temp.setTime(result.getTime());
			switch (groupNumber) {
				case 1 :
				case 2 :
					month = Integer.parseInt(matcher.group(groupNumber));
					day = Integer.parseInt(matcher.group(3 - groupNumber));
					result.set(Calendar.MONTH, month);
					result.set(Calendar.DAY_OF_MONTH, day);
					throwExceptionIfNotEqual(temp, result);
					assignPrecision(DAY_PRIME * MONTH_PRIME);
					if ((day != result.get(Calendar.DAY_OF_MONTH))
							|| ((month) != result.get(Calendar.MONTH)))
						throw new Exception(
								"[DeadlineParser].assignToCalendar(): Date Overflow");
					break;
				case 3 :
					minute = Integer.parseInt(matcher.group(2));
				case 4 :
					hour = Integer.parseInt(matcher.group(1));
					if (hour == 12 && regex.contains("m"))
						hour -= 12;
					hour += (matcher.group(0).contains("pm")) ? 12 : 0;
					result.set(Calendar.HOUR_OF_DAY, hour);
					result.set(Calendar.MINUTE, minute);
					result.set(Calendar.SECOND, 0);
					if (precision % TIME_PRIME != 0)
						throw new Exception(
								"[DeadlineParser].check(): Double Time Occurrence");
					precision /= TIME_PRIME;
					assert ((hour < 24) && (minute < 60)) : "Time Overflow in DeadlineParser";
					break;
				case 5 :
				case 6 :
					todayIs = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
					shift = (groupNumber == 6) ? 7 - todayIs
							+ Integer.parseInt(matcher.group(1)) : Integer.parseInt(matcher
							.group(1)) - todayIs;
					shift += (shift <= 0) ? 7 : 0;
					todayIs = Calendar.getInstance().get(Calendar.DAY_OF_YEAR);

					result.set(Calendar.DAY_OF_YEAR, todayIs + shift);
					throwExceptionIfNotEqual(temp, result);
					assignPrecision(DAY_PRIME * MONTH_PRIME * YEAR_PRIME);
					break;
				case 7 :
					year = Integer.parseInt(matcher.group(1));

					result.set(Calendar.YEAR, year);
					throwExceptionIfNotEqual(temp, result);
					assignPrecision(YEAR_PRIME);
					break;
				case 8 :
					month = Integer.parseInt(matcher.group(1));

					result.set(Calendar.MONTH, month);
					throwExceptionIfNotEqual(temp, result);
					assignPrecision(MONTH_PRIME);
					break;
				default :
					assert false : "Reached Default Case in DeadlineParser.check()";
			}
			arg = arg.replaceFirst(regex, " ");
		}
		return arg;
	}

	private void assignPrecision(int fields) {
		precision /= ((precision % YEAR_PRIME == 0) && (fields % YEAR_PRIME == 0)) ? YEAR_PRIME
				: 1;
		precision /= ((precision % MONTH_PRIME == 0) && (fields % MONTH_PRIME == 0)) ? MONTH_PRIME
				: 1;
		precision /= ((precision % DAY_PRIME == 0) && (fields % DAY_PRIME == 0)) ? DAY_PRIME
				: 1;
	}

	void throwExceptionIfNotEqual(Calendar a, Calendar b) throws Exception {
		if ((precision % YEAR_PRIME != 0)
				&& (a.get(Calendar.YEAR) != b.get(Calendar.YEAR)))
			throw new Exception(
					"[DeadlineParser].throwExceptionIfNotEqual(): NOT EQUAL");
		if ((precision % MONTH_PRIME != 0)
				&& (a.get(Calendar.MONTH) != b.get(Calendar.MONTH)))
			throw new Exception(
					"[DeadlineParser].throwExceptionIfNotEqual(): NOT EQUAL");
		if ((precision % DAY_PRIME != 0)
				&& (a.get(Calendar.DAY_OF_YEAR) != b.get(Calendar.DAY_OF_YEAR)))
			throw new Exception(
					"[DeadlineParser].throwExceptionIfNotEqual(): NOT EQUAL");
	}

	/**
	 * This function is used when time is not specified in the deadline. if limit
	 * is 0, then time is adjusted to 0:00:0 if limit is 1, then time is adjusted
	 * to 23:59:59
	 * 
	 * @param ref
	 * @param limit
	 * @return the ref's Calendar value with adjusted HOUR_OF_DAY, MINUTE, and
	 *         SECOND fields
	 */
	private Calendar adjustTime(Calendar ref, int limit) {

		Calendar adjustedRef = ref;
		if (precision == 1)
			return ref;
		if (precision % MONTH_PRIME == 0)
			adjustedRef.set(Calendar.MONTH, 11 * limit);
		if (precision % DAY_PRIME == 0)
			adjustedRef
					.set(Calendar.DAY_OF_MONTH,
							(adjustedRef.getActualMaximum(Calendar.DAY_OF_MONTH) - 1) * limit
									+ 1);
		adjustedRef.set(Calendar.HOUR_OF_DAY, 23 * limit);
		adjustedRef.set(Calendar.MINUTE, 59 * limit);
		adjustedRef.set(Calendar.SECOND, 59 * limit);
		return adjustedRef;
	}

	// ---------------------------------------------------------------------------------------------------------------------------\\

	private static final String[] KEY_WORD = { "from", "before", "due", "after" };
	private static final int[] KEY_COEFFICIENT = { 1, -1, 0, 1 };
	private static final int[] KEY_DEFAULT = { 0, 1, 0, 1 };
	private static final int[] KEY_LIMIT = { 0, 0, 1, 1 };// 0 for leftLimit, 2
																												// for rightLimit
	private static final int KEYWORDS_NUMBER = 4;

	/**
	 * 
	 * @param deadline
	 * @return the Calendar value of the deadline
	 * @throws Exception
	 *           In case of unacceptable format [One of the KEY_WORDs MUST be
	 *           present in the deadline]
	 */
	private Calendar getValue(String deadline) throws Exception {
		assert ((KEY_WORD.length == KEY_COEFFICIENT.length)
				&& (KEY_DEFAULT.length == KEY_LIMIT.length) && (KEYWORDS_NUMBER == KEY_WORD.length));
		deadline = deadline.trim();

		for (int i = 0; i < KEYWORDS_NUMBER; i++)
			if (deadline.contains(KEY_WORD[i])) {
				int totalTimeShift = KEY_COEFFICIENT[i]
						* getRange(deadline.split(KEY_WORD[i], 2)[0], KEY_DEFAULT[i]);
				Calendar res = getReference(deadline.split(KEY_WORD[i], 2)[1],
						KEY_LIMIT[i]);
				res.add(Calendar.SECOND, totalTimeShift);
				return res;
			}
		throw new Exception("[DeadlineParser].getValue():Unacceptable Format");
	}

	// ---------------------------------------------------------------------------------------------------------------------------\\

	private static final int SECONDS_IN_DAY = 60 * 60 * 24;
	private static final int SECONDS_IN_MINUTE = 60;
	private static final String[] rangeWord = { "years", "year", "y", "months",
			"month", "m", "week", "weeks", "w", "day", "days", "d", "hour", "hours",
			"h", "minutes", "minute", "min", "seconds", "sec" };
	private static final int[] rangeCoefficient = { 365 * SECONDS_IN_DAY,
			365 * SECONDS_IN_DAY, 365 * SECONDS_IN_DAY, 30 * SECONDS_IN_DAY,
			30 * SECONDS_IN_DAY, 30 * SECONDS_IN_DAY, 7 * SECONDS_IN_DAY,
			7 * SECONDS_IN_DAY, 7 * SECONDS_IN_DAY, SECONDS_IN_DAY, SECONDS_IN_DAY,
			SECONDS_IN_DAY, 60 * SECONDS_IN_MINUTE, 60 * SECONDS_IN_MINUTE,
			60 * SECONDS_IN_MINUTE, SECONDS_IN_MINUTE, SECONDS_IN_MINUTE,
			SECONDS_IN_MINUTE, 1, 1 };

	/**
	 * 
	 * @param range
	 * @param defaultValue
	 * @param format
	 * @return
	 * @throws Exception
	 */
	public static int getRange(String range, int defaultValue) throws Exception {
		assert (rangeWord.length == rangeCoefficient.length);

		range = range.trim();

		if (range.isEmpty())
			return defaultValue;

		int result = 0;
		String number = "";
		StringTokenizer st = new StringTokenizer(range);
		while (st.hasMoreTokens()) {
			boolean tokenIsNumeric = true;
			String token = st.nextToken();
			if (token.equals("and"))
				tokenIsNumeric = false;
			for (int i = 0; i < rangeWord.length; i++)
				if (token.equals(rangeWord[i])) {
					result += rangeCoefficient[i] * getNumericValue(number, 1);
					number = "";
					tokenIsNumeric = false;
				}
			number += (tokenIsNumeric) ? (" " + token) : "";
		}
		if (!number.trim().isEmpty())
			throw new Exception("[DeadlineParser].getRange(): Unidentified Chunk:"
					+ number);

		return result;
	}

	private static final String[] numbers = { "zero", "one", "two", "three",
			"four", "five", "siz", "seven", "eight", "nine", "ten", "eleven",
			"twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen",
			"eighteen", "nineteen", "twenty", "thirty", "fourty", "fifty", "sixty",
			"seventy", "eighty", "ninety" };
	private static final int[] values = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
			12, 13, 14, 15, 16, 17, 18, 19, 20, 30, 40, 50, 60, 70, 80, 90 };

	/**
	 * @param number
	 * @param defaultValue
	 * @return the int value of the number spelled in words. Range [zero-ninety
	 *         nine]
	 * @throws Exception
	 *           in case of unacceptable format of the number
	 */
	public static int getNumericValue(String number, int defaultValue)
			throws Exception {
		assert (values.length == numbers.length);

		number = number.toLowerCase().trim();

		if (number.isEmpty())
			return defaultValue;

		if (number.equals("zero"))
			return 0;

		try {
			return Integer.parseInt(number);
		} catch (Exception e) {
		}
		;

		int result = 0;

		StringTokenizer st = new StringTokenizer(number);
		int lastDigit = 0, secondLastDigit = 0;
		while (st.hasMoreTokens()) {
			String token = st.nextToken();
			boolean identified = false;
			for (int i = 1; i < values.length; i++)
				if (token.equals(numbers[i])) {
					lastDigit += (i <= 19) ? 1 : 0;
					secondLastDigit += (i >= 10) ? 1 : 0;
					result += values[i];
					identified = true;
				}
			if (!identified)
				throw new Exception(
						"[DeadlineParser].getNumericValue(): Cannot Identify Token: "
								+ number);
		}
		if (lastDigit > 1 || secondLastDigit > 1)
			throw new Exception(
					"[DeadlineParser].getNumericValue(): Unacceptable Numeric Format");

		return result;
	}

	/**
	 * 
	 * @param arg
	 * @return Returns string trimmed in such a way that there are no two
	 *         consecutive whitespaces and no articles
	 * @throws Exception
	 *           Unable to process empty strings
	 */
	public static String getRidOfRedundantSpacesAndArticles(String arg)
			throws Exception {
		arg = " " + arg + " ";
		arg = arg.replace(" the ", " ");
		arg = arg.replace(" an ", " ");
		arg = arg.replace(" a ", " ");
		arg = arg.trim();

		if (arg.isEmpty())
			throw new Exception(
					"[DeadlineParser].getRidOfRedundatnSpacesAndArticles(): Empty String");

		String str = "" + arg.charAt(0);
		for (int i = 1; i < arg.length(); i++)
			str += ((arg.charAt(i) != ' ') || (arg.charAt(i - 1) != ' ')) ? arg
					.charAt(i) : "";

		return str;
	}

	/**
	 * 
	 * @param arg
	 * @param timeShift
	 * @return postponed date(arg)
	 * @throws Exception
	 *           In case of unacceptable format of the timeShift
	 */
	public static Date postpone(Date arg, String timeShift) throws Exception {
		Calendar postponed = Calendar.getInstance();
		postponed.setTime(arg);
		postponed.add(Calendar.SECOND, getRange(timeShift, 0));
		return postponed.getTime();
	}

	private static final String[][] toReplace = { { "first ", "1 " },
			{ "second ", "2 " }, { "third ", "3 " }, { "fourth ", "4 " },
			{ "fifth ", "5 " }, { "sixth ", "6 " }, { "seventh ", "7 " },
			{ "eighth ", "8 " }, { "ninth ", "9 " }, { "tenth ", "10 " },
			{ "eleventh ", "11 " }, { "twelfth ", "12 " }, { "thirteenth ", "13 " },
			{ "fourteenth ", "14 " }, { "fifteenth ", "15 " },
			{ "sixteenth ", "16 " }, { "seventeenth ", "17 " },
			{ "eighteenth ", "18 " }, { "nineteenth ", "19 " },
			{ "twentieth ", "20 " }, { "thirtieth ", "30 " }, { "twenty ", "2" },
			{ "thirty ", "3" }, { "january", "<0>" }, { "february", "<1>" },
			{ "march", "<2>" }, { "april", "<3>" }, { "may", "<4>" },
			{ "june", "<5>" }, { "july", "<6>" }, { "august", "<7>" },
			{ "september", "<8>" }, { "october", "<9>" }, { "november", "<10>" },
			{ "december", "<11>" }, { "jan", "<0>" }, { "feb", "<1>" },
			{ "mar", "<2>" }, { "apr", "<3>" }, { "may", "<4>" }, { "jun", "<5>" },
			{ "jul", "<6>" }, { "aug", "<7>" }, { "sep", "<8>" }, { "oct", "<9>" },
			{ "nov", "<10>" }, { "dec", "<11>" }, { "sunday ", "[1] " },
			{ "monday ", "[2] " }, { "tuesday ", "[3] " }, { "wednesday ", "[4] " },
			{ "thursday ", "[5] " }, { "friday ", "[6] " }, { "saturday ", "[7] " },
			{ " am", "am" }, { " pm", "pm" }, { "sun ", "[1] " }, { "mon ", "[2] " },
			{ "tue ", "[3] " }, { "wed ", "[4] " }, { "thu ", "[5] " },
			{ "fri ", "[6] " }, { "sat ", "[7] " }, { "1st ", "1 " },
			{ "2nd ", "2 " }, { "3rd ", "3 " }, { "4th ", "4 " }, { "5th ", "5 " },
			{ "6th ", "6 " }, { "7th ", "7 " }, { "8th ", "8 " }, { "9th ", "9 " },
			{ "0th ", "0 " }, { "11th ", "11 " }, { "12th ", "12 " },
			{ "13th ", "13 " } };
	private static final int itemsToReplace = 76;

	public static String remake(String st) {
		assert (itemsToReplace == toReplace.length) : "adjust the value of DeadlineParser.itemsToReplace";

		st = " " + st + " ";
		for (int i = 0; i < itemsToReplace; i++)
			st = st.replaceAll(toReplace[i][0], toReplace[i][1]);

		return st.trim();
	}

	public static ArrayList<Date> overlap(Date date, int frequencyInSecs,
			int numberOfOccurrences, Date firstOc) {
		ArrayList<Date> returnValue = new ArrayList<Date>();
		Calendar dummy = Calendar.getInstance();
		dummy.setTime(date);
		dummy.set(Calendar.HOUR_OF_DAY, 0);
		dummy.set(Calendar.MINUTE, 0);
		dummy.set(Calendar.SECOND, 0);
		long leftLimitInSecs = dummy.getTimeInMillis() / 1000;
		long event = firstOc.getTime() / 1000;
		for (int i = 0; i < numberOfOccurrences; i++) {
			if (event >= leftLimitInSecs
					&& event < (leftLimitInSecs + SECONDS_IN_DAY))
				returnValue.add(new Date(event * 1000));
			event += frequencyInSecs;
		}
		return returnValue;
	}
}
